<?php
/**
 * @package    system
 * @subpackage service
 */
class ServicesSchemas
{
    const INPUT_SCHEMA        = 1;
    const OUTPUT_SCHEMA       = 2;
    const NO_PARAMETER_SCHEMA = '{"type":"null"}';
    const VOID_SCHEMA         = '{"type":"null"}';
    const JSON_DOC_PARAM      = 'json';

    /**
     * Indicate that the schemas will write and retrieved from file cache.
     *
     * @var bool
     */
    public static $useFileCache = false;

    /**
     * The schema cache dir
     *
     * @var string
     */
    protected static $schemaDir  = null;


    /**
     * Find the file with the schema definition.
     *
     * @param string $class The name of the service class to look for the schema.
     * @param string $method The name of the service method to look for the schema.
     * @param string $schemaType The name of the extra specification of the schema (input/output)
     * @return StdClass The JSON schema converted to php objects, to run against the validator.
     *
     * @todo Write a php code serialized schema to improve performance.
     */
    static function getSchema($class, $method, $schemaType)
    {
        $filePath = self::getSchemaDir() . $class . '.' . $method . '.' . $schemaType . '.yml';
        if(self::$useFileCache === true && file_exists($filePath)) {
            $jsonSchema = file_get_contents($filePath);
            return json_decode($jsonSchema);
        }
        else {
            $schema = self::extractSchema($class, $method, $schemaType);
            return json_decode($schema);
            throw new ServiceSchemaNotFoundException('The schema '.$filePath.' was not found. ');
        }
    }

    /**
     * Find the file with the input schema definition
     *
     * @param  string   $class
     * @param  string   $method
     * @return StdClass The JSON schema
     */
    public static function getInputSchema($class,$method)
    {
        return self::getSchema($class,$method, self::INPUT_SCHEMA);
    }

    /**
     * Find the file with the output schema definition
     *
     * @param  string   $class
     * @param  string   $method
     * @return StdClass The JSON schema
     */
    public static function getOutputSchema($class,$method)
    {
        return self::getSchema($class,$method, self::OUTPUT_SCHEMA);
    }

    /**
     * Return the schema type name
     *
     * @param  string $type
     * @return string
     */
    static function getSchemaTypeName($type)
    {
        if($type == self::INPUT_SCHEMA) return 'input/param';
        if($type == self::OUTPUT_SCHEMA) return 'output/return';
    }

    /**
     * Return the schema cache dir
     *
     * @return string
     */
    static function getSchemaDir()
    {
        if (null == self::$schemaDir) {
            self::setSchemaDir(sfConfig::get('sf_cache_dir'). DIRECTORY_SEPARATOR .'serviceSchemas');
        }
        return rtrim(self::$schemaDir) . DIRECTORY_SEPARATOR;
    }

    /**
     * Set the schema dir
     *
     * @param string $schemaDir
     */
    static function setSchemaDir($schemaDir)
    {
        if (!is_dir($schemaDir)) {
            throw new ServiceSchemaException('The schema dir "' . $schemaDir . '" is not a valida directory');
        }
        self::$schemaDir = $schemaDir;
    }

    /**
     * Verify if the schema is a valid json and writes it on disk to be used as a cache.
     *
     * @todo validate the schema definition against the json schemas schema.
     */
    static function writeSchema($schema,$class,$method,$schemaType)
    {
        if(!is_object(json_decode($schema))) {
            $msg = "Invalid schema for ".$class." - ".$method." - ".self::getSchemaTypeName($schemaType)." (did you put the pre tag?):\n".'--'.$schema."--";
            throw new ServiceInvalidSchemaException($msg);
        }
        else {
            $filePath = self::getSchemaDir() . $class . '.' . $method . '.' . $schemaType . '.yml';
            file_put_contents($filePath,$schema);
        }
    }

    /**
     * Extract input schema from class method comment
     *
     * @param string $class
     * @param string $method
     * @return null|string
     */
    public static function extractInputSchema($class,$method)
    {
        return self::extractSchema($class,$method,self::INPUT_SCHEMA);
    }

    /**
     * Extract output schema from class method comment
     *
     * @param string $class
     * @param string $method
     * @return null|string
     */
    public static function extractOutputSchema($class,$method)
    {
        return self::extractSchema($class,$method,self::OUTPUT_SCHEMA);
    }

    /**
     * Extract schema from method
     *
     * @param  string $class
     * @param  string $method
     * @param  string $schemaType
     * @return null|string
     */
    public static function extractSchema($class,$method,$schemaType)
    {
        // Validando os dados de entrada
        if (!class_exists($class)) {
            throw new ServiceException('The class' . $class . 'doesnt exist');
        }
        $reflectionMethod = new ReflectionMethod($class,$method);
        $docBlock        = $reflectionMethod->getDocComment();
        $schema          = '';

        if ($schemaType == self::INPUT_SCHEMA) {
            if ($reflectionMethod->getNumberOfParameters() > 1) {
                throw new ServiceSchemaException("Services must have zero ore one parameter, no more.");
            }
            // Constroi o esquema a partir dos parametros do docblock
            $docBlock   = preg_replace("/@return(.*)/sm", '', $docBlock);
            preg_match_all("/@param\s+([^\s]+)\s+\\$([\w]+)\s+([^@<]+)?/s", $docBlock, $matches, PREG_SET_ORDER);

            if ($reflectionMethod->getNumberOfParameters() == 0) {
                if (count($matches) > 0) {
                    throw new ServiceSchemaException ("There is a parameter documented, but the method declares none.");
                }
                $schema = self::NO_PARAMETER_SCHEMA;
            } else {
                $inputSchema = new stdClass;
                $inputSchema->type       = 'object';
                $inputSchema->properties = new stdClass;

                $jsonBlockValidate = false;
                foreach ($matches as $match) {
                    $property = new stdClass;
                    $property->type        = self::buildPropertyType($match[1]);
                    $paramDescription = self::extractDescription($match[3]);

                    if ($paramDescription != '') {
                        $property->description= $paramDescription;
                    }
                    $jsonInline = self::extractJson($match[3],true);

                    if (array_key_exists($match[2],$inputSchema->properties)) {
                        self::objectSchemaExtend($inputSchema->properties->$match[2], $property);
                    } else {
                        $inputSchema->properties->{$match[2]} = $property;
                    }
                    self::objectSchemaExtend($inputSchema->properties->{$match[2]}, $jsonInline);

                    if ($match[2] == self::JSON_DOC_PARAM || count($matches) == 1) {
                        $inputSchema = $property;

                        /*if (($property->type == 'object' || $property->type == 'array') && !$jsonInline) {
                            $jsonBlockValidate = true;
                        }*/
                        break;
                    }
                }
                // Params general input schema
                $jsonBlock   = self::extractJson($docBlock, true, false, true);

                if ($jsonBlock == null) {
                    $schema = false;
                } else {
                    $inputSchema = self::objectSchemaExtend($inputSchema,$jsonBlock);
                    $inputSchema->type = self::buildPropertyType($inputSchema->type);

                    if ($inputSchema->type != 'object') {
                        unset($inputSchema->properties);
                    }
                    $schema  = json_encode($inputSchema);
                }
            }
        }
        if ($schemaType == self::OUTPUT_SCHEMA) {
            if (preg_match("/@return\s+(\S+)\s+(.*?)(<pre>.*?)?(@|\*\/)/sm",$docBlock,$matches)) {
                if ($matches[1] == 'void') {
                    $schema = self::VOID_SCHEMA;
                } else {
                    $outputSchema= new stdClass;
                    $returnType  = self::buildPropertyType($matches[1]);

                    $jsonInline  = self::extractJson($matches[2], true);
                    $jsonBlock   = self::extractJson('..return' . $matches[3], true);
                    $outputDesc  = self::extractDescription($matches[2]);

                    $outputSchema = new stdClass;
                    $outputSchema->type = $returnType;

                    if ($outputDesc != '') {
                        $outputSchema->description = $outputDesc;
                    }
                    if (is_object($jsonInline)) $outputSchema = self::objectSchemaExtend($outputSchema,$jsonInline);
                    if (is_object($jsonBlock))  $outputSchema = self::objectSchemaExtend($outputSchema,$jsonBlock);
                    $schema = json_encode($outputSchema);
                }
            }
        }
        if($schema) {
            if (self::$useFileCache == true) {
                self::writeSchema($schema, $class, $method, $schemaType);
            }
            return $schema;
        }
        else {
	        throw new ServiceInvalidSchemaException('The schema for '.$class.'::'.$method.' - '.self::getSchemaTypeName($schemaType).' was not found on the doc block.');
        }
    }

    /**
     * Construindo propriedades para o json schema
     *
     * @param  string|array
     * @return string|array
     */
    public static function buildPropertyType($val,$include='')
    {
        if (is_string($val)) {
            $val = trim($val);

            if ($include != '') $val .= '|'.$include;
            $parts = explode("|",$val);

            if (count($parts)>1) {
                $type = self::buildPropertyType($parts);
            } else {
                switch ($val) {
                    case 'string':
                    case 'array':
                    case 'object':
                        $type = $val;
                        break;
                    case 'int':
                    case 'integer':
                        $type = 'integer';
                        break;
                    case 'float':
                    case 'number':
                        $type = 'number';
                        break;
                    case 'bool':
                    case 'boolean':
                        $type = 'boolean';
                        break;
                    case 'null':
                    case 'void':
                        $type = 'null';
                        break;
                    case 'mixed':
                        $type = self::buildPropertyType(array());
                    default:
                        $type = 'any';
                }
            }
        } else if (is_array($val)) {
            if ($include != '') $val = array_merge($val,array($include));
            $types = array();
            foreach($val as $key => $val) {
                $types= array_merge($types,array(self::buildPropertyType($val)));
            }
            $type = array_unique($types);
        }
        return $type;
    }

    /**
     * Extract help text from class method comment string
     *
     * @param string $docblock
     */
    public static function extractHelpText($docBlock)
    {
        $helpText = '';

        if (preg_match(':/\*\*\s*\r?\n\s*\*\s(.*?)\r?\n\s*\*(\s@|/):s', $docBlock, $matches)) {
            $helpText = $matches[1];
            $helpText = preg_replace('/(^\s*\*\s)/m', '', $helpText);
            $helpText = preg_replace('/\r?\n\s*\*\s*(\r?\n)*/s', "\n", $helpText);
            $helpText = trim($helpText);
        }

        return $helpText;
    }

    /**
     * Extract json from a string
     *
     * @param  string $string
     * @param  bool   $decode If the output will encoded with json decode
     * @return string|object  String, if json string finded, object if the finded string will be decoded
     */
    public static function extractJson($string, $decode=false, $asArray=false, $preTag=false)
    {
        $output = '';

        $string = preg_replace("/(^\s*\*\s)/m", '', $string);
        $string = preg_replace("/\r?\n\s*\*\s*(\r?\n)*/s", "\n", $string);
        $string = preg_replace("/(\r?\n(\s+|\t+))/s", "", $string);
        $string = preg_replace("/(\r?\n)/s","",$string);

        $pattern = "/(\{.*\})/s"; // @todo melhorar esse pattern
        if ($preTag) {
            $pattern = "/<pre>(.*?)<\/pre>/sU";
        }
        if (preg_match($pattern, $string, $match)) {
            $output = $match[1];
        }
        if ($output !== '' && $decode !== false) {
            $output = json_decode($output,$asArray);
        }
        return $output;
    }

    /**
     * Extract coments from docblock string.
     *
     * @param string $string
     * @param bool $stripJson
     * @param bool $stripHtml
     */
    public static function extractDescription($string, $stripJson = true, $stripHtml = true)
    {
        $string = preg_replace('/\s?\*\s/m', '', $string);
        $string = preg_replace('/\s{2,}/', ' ',  $string);
        $string = preg_replace('/<pre>(.*?)<\/pre>/s', '', $string);

        if ($stripJson) {
            $string = preg_replace("/\{.*\}/s",'', $string);
        }
        if ($stripHtml) {
            //$string = preg_replace("/<([\w]+)(.*?)>(.*?)<\/\1>/",'',$string);
            $string = strip_tags($string);
        }
        return trim($string);
    }

    /**
     * Merge/extend an object from other object
     *
     * @param object  $destination
     * @param object  $source
     * @return object The extended object
     */
    public static function objectSchemaExtend($destination, $source, $recursive=true)
    {
        if (is_object($destination) && is_object($source)) {
            foreach ($source as $property => $value) {
                if (is_object($value) && array_key_exists($property,$destination) && is_object($destination->$property)) {
                    $destination->$property = self::objectSchemaExtend($destination->$property, $value, $recursive);
                } else {
                    $destination->$property=$value;
                }
            }
        } else if (is_array($source) && is_array($destination)) {
            $destination = array_merge($destination,$source);
        }
        return $destination;
    }
}
?>